package hwip.processing;

import java.net.Socket;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

import org.json.JSONException;
import org.json.JSONObject;
import org.osgi.framework.BundleContext;
import org.osgi.util.tracker.ServiceTracker;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.database.Cursor;
import android.util.Log;
import de.hmm.hwip.configuration.api.ISpotConf;
import de.hmm.hwip.connection.api.IConOut;
import de.hmm.hwip.deviceconfig.DeviceConfiguration;
import de.hmm.hwip.general.IAndroidContext;
import de.hmm.hwip.general.IMessageContent;
import de.hmm.hwip.security.api.ISecurityClient;
import de.hmm.hwip.security.api.ISecurityServer;
import de.hmm.hwip.storage.api.ISQLite;
/**
 * Spot Service
 * 
 * Receiver and Sender of System actions between spot and Gateway
 * manages added spots, automatically send spot data to other spots  
 * 
 * @author johannes mendel
 * @version 05.11.2012
 *
 */
public class Spot implements IMessageContent {

	/**
	 * Class TAG
	 */
	private final String TAG = "Spot Message";
	
	/**
	 * security flag
	 */
	boolean securityFlag;
	
	/**
	 * variable needed for service tracker 
	 */
	private BundleContext context;
	
	/**
	 * spot config service tracker
	 */
	private ServiceTracker serviceTrackerConf;
	
	/**
	 * database service tracker
	 */
	private ServiceTracker serviceTrackerDB;
	
	/**
	 * OutConnection service tracker
	 */
	private ServiceTracker serviceTrackerOutCon;
	
	/**
	 * Android Context service tracker
	 */
	private ServiceTracker contextServiceTracker;
	
	/**
	 * Server Security service tracker
	 */
	private ServiceTracker ssecuServiceTracker;
	
	/**
	 * Client Security service tracker
	 */
	private ServiceTracker csecuServiceTracker;
	
	/**
	 * serivce for configuration, got by service tracker
	 */
	private ISpotConf serviceConf;
	
	/**
	 * serivce for datasource, got by service tracker
	 */
	private ISQLite serviceDB;
	
	/**
	 * service for outgoing connection to answer receiver
	 */
	private IConOut serviceOutCon;
	
	/**
	 * service for Android App Context
	 */
	private IAndroidContext androidContextService;
	
	/**
	 * service for security server site
	 */
	private ISecurityServer serviceServerSecurity;
	
	/**
	 * service for security client site
	 */
	private ISecurityClient serviceClientSecurity;
	
	/**
	 * query for this service
	 */
	private String query;
	
	/**
	 * result of the query
	 */
	private Cursor queryResult;
	
	/**
	 * result of insert query
	 */
	private int insertOk = 0;

	/**
	 * type of query 
	 * 1 - select
	 * 2 - insert/Update/delete
	 * 3 - multiinsert
	 * 4 - get complete db copy
	 * 9 - no query
	 */
	private int queryType = 9;
	
	/**
	 * identifier of actions after query has been executed
	 * 1 - if spot is not in id set query to ask for last id  				: Gateway
	 * 2 - got last Spot-Id send it to spot									: Gateway
	 * 3 - received last id and set new IPAddress. send new Ip to Gateway	: Spot
	 * 4 - spot has added to db generate a copy of db						: Gateway
	 * 5 - db has been copied and will be send to Spot						: Gateway
	 * 6 - send new spot to every spot in db								: Gateway
	 * 9 - Db copied to spot -> registration finished						: Spot
	 */
	private int answerAction = 0;
	
	/**
	 * calling socket connection
	 */
	private Socket receiver;
	
	/** Android App Context **/
	private Context appContext;
	
	/**
	 * member to store error from data source connection
	 */
	private String errorDB = "";
	
	/**
	 * Jsonobject of sended data by client
	 */
	private JSONObject payload;
	
	/**
	 * waiting for database feedback
	 */
	private boolean wait = true;
	
	/** gets value of new Ip */
	private String changedIp = null;
	
	/** map with complete DB copy */
	private Map<String, Object> completeDb;
	
	/** Jsonobject with complete DB copy */
	private JSONObject completeDbJson;
	
	/**Filed of target by burst message **/
	private String[] targets;
	
	/** Map to store payload **/
	private Map<String, Object> data;
	
	/** Class to Configurate Network **/
	DeviceConfiguration devCon;
	
	@Override
	public void run() {
		// getting the configuration-unit-service
		serviceTrackerConf = new ServiceTracker(this.context, ISpotConf.class.getName(), null );
		serviceTrackerConf.open();
		
		// getting the datasource service
		serviceTrackerDB = new ServiceTracker(this.context, ISQLite.class.getName(), null );
		serviceTrackerDB.open();
		
		// getting the android context service
		contextServiceTracker = new ServiceTracker(this.context, IAndroidContext.class.getName(), null );
		contextServiceTracker.open();
		
		//getting the security services
		ssecuServiceTracker = new ServiceTracker(this.context, ISecurityServer.class.getName(), null);
		ssecuServiceTracker.open();
		csecuServiceTracker = new ServiceTracker(this.context, ISecurityClient.class.getName(), null);
		csecuServiceTracker.open();
		
		/** wait for OSGi-Services to come online */
		synchronized (this) {
			while(true){
				androidContextService = (IAndroidContext) contextServiceTracker.getService();
				if(androidContextService != null){
					break;
				}else{
					try {
						wait(2000);
					} catch (InterruptedException e) {
						Log.e(TAG, "wait for DB Service exception: " + e.getMessage());
					}
				}
			}
			
			while(true){
				serviceConf = (ISpotConf) serviceTrackerConf.getService();
				if(serviceConf != null){
					break;
				}else{
					try {
						wait(2000);
					} catch (InterruptedException e) {
						Log.e(TAG, "wait for DB Service exception: " + e.getMessage());
					}
				}
			}
			
			while(true){
				serviceDB = (ISQLite) serviceTrackerDB.getService();
				if(serviceDB != null){
					break;
				}else{
					try {
						wait(2000);
					} catch (InterruptedException e) {
						Log.e(TAG, "wait for DB Service exception: " + e.getMessage());
					}
				}
			}
			
			while(true){
				serviceServerSecurity = (ISecurityServer) ssecuServiceTracker.getService();
				if(serviceServerSecurity != null){
					break;
				}else{
					try {
						wait(2000);
					} catch (InterruptedException e) {
						Log.e(TAG, "wait for Server Security Service exception: " + e.getMessage());
					}
				}
			}
			
			while(true){
				serviceClientSecurity = (ISecurityClient) csecuServiceTracker.getService();
				if(serviceClientSecurity != null){
					break;
				}else{
					try {
						wait(2000);
					} catch (InterruptedException e) {
						Log.e(TAG, "wait for Client Security Service exception: " + e.getMessage());
					}
				}
			}
		}
		
		// getting payload from json to map
		data = new LinkedHashMap<String, Object>();
		try {
			data = JsonToMap(payload);
		} catch (JSONException e) {
			Log.e(TAG, "Json Exception: " + e.getMessage());
		}
		
		if(androidContextService != null){
			appContext = androidContextService.transferContext();
			devCon = new DeviceConfiguration(appContext);
			
			showDatainGui();
		}
		
		if(serviceConf != null)
		{
			performDbRequest(null);
		}
	}

	/**
	 * sets query for DataBase request
	 * @param redbrequest : if a queryresult claims a new DB request
	 */
	private void performDbRequest(String redbrequest){
		wait = true;
		if(serviceDB != null){
			
			//if there is a second request
			if(redbrequest == null){
				if (((String) data.get("command")) != null)	// received command
				{
		
					String command = ((String) data.get("command"));
					
					if(command.equals("newgateway")){
						if(setNetworkIp(201)){
							queryType = 2;
							query = serviceConf.getQueryForUpdateSpot(1 , getIpAddress(), 1); 
							setSharedSpotPrefs(getIpAddress(), 1, true, true);
							answerAction = 9;
						}
					}else if(command.equals("setip")){
		    			//set ip address of the spot and send it to Gateway back + request db copy
						Log.i(TAG, "Spot is not in db till now ... set given id to ip adress and set shared prefs");
						
						if(data.containsKey("security")){
							setSharedCryptoPrefs((String) data.get("security"));
						}
						
		    			if(setNetworkIp((Integer) data.get("value"))){
		    				queryType = 1;
		    				query = serviceConf.getQueryForSpotIds();
		    				queryResult = null;
		    				answerAction = 3;
		    				setSharedSpotPrefs(this.receiver.getInetAddress().getHostAddress(), (Integer) data.get("value"), false, false );
		    				answerReceiver();
		    				return;
		    			}
		    		}else if(command.equals("newspot")){ 
		    			//sets Spot to db wit standard room 1
		    			queryType = 2;
		    			query = serviceConf.getQueryForSetSpot((Integer) data.get("id"), 1, ((String)data.get("ip_address")));	
		    			answerAction = 4;
		    		}else if(command.equals("filldb")){
		    			Log.i(TAG, "fills db with data received by Gateway");
		    			queryType = 3;	
		    			setSharedSpotPrefs(null, 0, false, true);
		    			answerAction = 9;
		    		}else if (command.equals("setspot")){
		    			queryType = 2;
		    			answerAction = 9;
		    			query = serviceConf.getQueryForSetSpot((Integer) data.get("id"),(Integer) data.get("room_id"), (String) data.get("ip_address"));
		    		}else if(command.equals("getaeskey")){
		    			queryType = 9;
		    			answerAction = 7;
		    			query = null;
		    		}else if(command.equals("setaeskey")){
		    			queryType = 9;
		    			answerAction = 9;
		    			query = null;
		    			setSharedCryptoPrefs((String) data.get("security"));
		    		}else if (((String) data.get("param")).equals("ip") && command.equals("existspot"))
		    		{
						// getting ips from Spots
						queryType = 1;
						query = serviceConf.getQueryForSpotId(((String) data.get("value")));
						answerAction = 1;
		    		}
				}
			}
			else if(redbrequest.equals("lastspotid")){
				queryType = 1;
				query = serviceConf.getQueryForSpotIds();
				answerAction = 2;
			} else if(redbrequest.equals("getcompletedb")){
				queryType = 4;
				answerAction = 5;
			}else if( redbrequest.equals("getspots")){
				queryType = 1;
				query = serviceConf.getQueryForSpots();
				answerAction = 6;
			}
		
			serviceDB.transferData(this);
			/*
			 * Synchronizing for the purpose that this thread can wait until
			 * the DB thread is finished. If this is the case the DB thread
			 * calls the synchronized <code>stopWaiting()</code> method and so
			 * becomes the monitor of this thread. The thread will get notified
			 * and stops waiting.
			 */
			synchronized (this) 
			{
				while(wait)
				{
					try 
					{
						Log.i(TAG, "Waiting for Db to finish");
						this.wait();
					} 
					catch (InterruptedException e) 
					{
						errorDB += "Could not execute request!";
						Log.e(TAG, "Syncerror: " + e.getMessage());
					}
				}
			}
			
			Log.i(TAG, "Data received from DB");
			// generate answer to receiver
			answerReceiver();
		}
	}
	
	/**
	 * answer the the end-device
	 */
	@Override
	public void answerReceiver() {
		// getting the configuration-unit-service
		serviceTrackerOutCon = new ServiceTracker(this.context, IConOut.class.getName(), null );
		serviceTrackerOutCon.open();
		serviceOutCon = (IConOut) serviceTrackerOutCon.getService();
		
		if(serviceOutCon != null){
			JSONObject answer = generateAnswer();
			if(answer != null){
				if(answerAction == 6){
					serviceOutCon.startBurstCon(targets , this.context, answer, true);
				}else{
					serviceOutCon.startCon(this.receiver, this.context, answer, securityFlag);
					if(answerAction == 5){
						Log.i(TAG, "read all spots from db to send new Spot to");
						performDbRequest("getspots");
					}
				}
			}
		}

	}

	@Override
	public void setBundleContext(BundleContext context) {
		this.context = context;

	}

	@Override
	public void setConnection(Socket connection) {
		this.receiver = connection;

	}

	@Override
	public void setPayload(JSONObject payload) {
		this.payload = payload;

	}

	@Override
	public void setSecurityFlag(boolean flag) {
		this.securityFlag = flag;

	}

	@Override
	public synchronized void stopWaiting() {
		this.wait = false;
		this.notify();
	}

	@Override
	public JSONObject generateAnswer() {
		JSONObject answer = new JSONObject();
		
		try {
			answer.put("service", payload.get("service"));
		
			//checking if there had been an error by database requests
			if (errorDB.equals(""))
			{
				switch(answerAction){
				case 1: 	
					Log.i(TAG, "New spot connects and asks for reference in db -> db query for last id");
					if(queryType == 1){
						performDbRequest("lastspotid");
						return null;
					}
					break;
					
				case 2: 
					if(queryResult.moveToLast()){
						//last position in column 1
						int last = queryResult.getInt(0);
						
						if(last < 201){
							last += 201;
						}
						
						if(last == devCon.getDhcp() || last == devCon.getDNS() || last == devCon.getNetGateway()){
							last++;
						}

						answer.put("command", "setip");
						answer.put("value", last+1);
						answerAction = 7;
						
						securityFlag = false;
						
						Log.i(TAG, "Spot is not in Db. Send last id of spots + 1 ("+ (last + 1) +") to spot" );
						
						break;
					}
						
				case 3: 
					Log.i(TAG, "Ip adress is set (" + changedIp + ") ask for complete db");
					//if(queryResult == null && query == "getdb"){
						// ip from Db id is set and spot ask for db fill
						answer.put("command", "newspot");
						answer.put("ip_address", changedIp);
						answer.put("id", data.get("value"));
					//}
						break;
					
				case 4: 
					Log.i(TAG, "New Spot has been set to DB -> now read complete DB to send back");
					if(insertOk == 1){
						//when spot has added to db generate a copy of db
						performDbRequest("getcompletedb");
						return null;
					}
						break;
					
				case 5: 
					Log.i(TAG, "send complete DB");
					return generateDbCopyAnswer();
				
				case 6:
					Log.i(TAG, "send new spot with all data to all spots");
					getTargets();
					if(queryResult.moveToLast()){
						String ip = queryResult.getString(2);
						int room_id = queryResult.getInt(1);
						int id = queryResult.getInt(0);
						answer.put("id", id);
						answer.put("command", "setspot");
						answer.put("ip_address", ip);
						answer.put("room_id", room_id);
					}
						break;
				case 7:
						if(query == null){
							answer.put("command", "setaeskey");
						}
						break;
				case 9: 
					Log.i(TAG, "finished ... nothing to do anymore");
					return null;
				}
				if(answerAction == 7){
					String key = getEncryptedKey();
					answer.put("security", key);
					Log.i(TAG, "AES Security Key added.");
				}
			}else{
				Log.e(TAG, "Data Base failure. Don't answer receiver.");
				return null;
			}
		
		} catch (JSONException e) {
			Log.e(TAG, "Json Exception: " + e.getMessage());
		}
		
		return answer;
	}

	@Override
	public String getQuery() {
		return this.query;
	}

	@Override
	public int getQueryType() {
		return this.queryType;
	}

	@Override
	public void setErrorResult(String error) {
		this.errorDB = error;
		
	}

	@Override
	public void setQueryResult(Cursor result) {
		this.queryResult = result;
		
	}

	@Override
	public void setQueryResult(int result) {
		this.insertOk = result;
		
	}
	
	@Override
	public void setCompleteDbResult(JSONObject db) {
		this.completeDbJson = db;
		
	}
	
	@Override
	public Map<String, Object> getCompleteDB() {
		return this.completeDb;
	}
	
	/** Method to set Ip to given id (ip) from Database 
	 * @param ip - last oktett */
	private boolean setNetworkIp(int ip){
		boolean done  = false;
		changedIp = devCon.setIp(ip);
		Log.i(TAG, "new IP: " + changedIp);
		if(changedIp != null)
			done = true;
			
		return done;
	}
	
	/** Method to get Ip-Address */
	private String getIpAddress(){
		String ipAddress;
		
		//DeviceConfiguration devCon = new DeviceConfiguration(appContext);
		ipAddress = devCon.getIp();
			
		return ipAddress;
	}
	
	/** Generates JsonObject with complete DB copy **/
	private JSONObject generateDbCopyAnswer(){
		JSONObject answer = new JSONObject();
		
			try {
				answer.put("service", payload.get("service"));
				answer.put("command", "filldb");
				answer.put("db" , completeDbJson);
								
			} catch (JSONException e) {
				Log.e(TAG, "Json Exception: " + e.getMessage());
			}
			
		return answer;
	}
	
	/** transform a JsonObject to a Map **/
	@SuppressWarnings({ "rawtypes"})
	private Map<String, Object> JsonToMap(JSONObject object) throws JSONException {
		Map<String, Object> map = new LinkedHashMap<String, Object>();
		Iterator keys = object.keys();
		while (keys.hasNext()) {
			String key = (String) keys.next();
			if(key.equals("db")){
				completeDb = transformCompleteDb(object.getJSONObject(key));
			}else
				map.put(key, object.get(key));
			
		}
		return map;
	}
	
	@SuppressWarnings("rawtypes")
	private Map<String, Object> transformCompleteDb(JSONObject db){
		Map<String, Object> dbMap = new LinkedHashMap<String, Object>();
		
		Iterator tables = db.keys();
		while (tables.hasNext()) {
			String table = (String) tables.next();
			try {
				dbMap.put(table, JsonToMap(db.getJSONObject(table)));
			} catch (JSONException e) {
				Log.e(TAG, "Json exception: " + e.getMessage());
			}
		}
		
		return dbMap;
	}
	
	/**
	 * sets target ip adresses for spot burst
	 */
	private void getTargets(){
		//field index
		int i = 0;
		int rowcount = queryResult.getCount();
		targets = new String[rowcount-1];
		//copy results to field
		queryResult.moveToFirst();
		while(queryResult.moveToNext() || i < rowcount - 1){
			targets[i] = queryResult.getString(2);
			i++;
		}
	}	
	
	/**
	 * Sets Spot and Gatewayspecification 
	 * @param ipAddress : ip-adress of Gateway
	 * @param spotid : id of this spot
	 * @param isgate : identifier if this spot is gateway
	 * @param regFinished: identifier if registration has finished
	 */
    private void setSharedSpotPrefs(String ipAddress, int spotid, boolean isgate, boolean regFinished){
    	SharedPreferences settings = appContext.getSharedPreferences("hwip", Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = settings.edit();
        if(spotid != 0){
        	editor.putInt("spotId", spotid);
        }
        if(ipAddress != null){
        	editor.putString("gatewayip", ipAddress);
        }
        editor.putBoolean("isgateway", isgate);
        editor.putBoolean("regfinished", regFinished);
        editor.commit();
    }
    

	/**
	 * Sets the Cryptographical shared preferences
	 * AES_KEY : crypted key
	 * AES_KEY_DATE : current date, because key is refreshed after 24h 
	 * They are used to de and encrypt stream data
	 * @param encryptedKey : RSA encrypted RSA key
	 */
    private void setSharedCryptoPrefs(String encryptedKey){
		String decryptedKeyString = serviceClientSecurity.decryptStringRSA(encryptedKey);

		SharedPreferences preferences = appContext.getSharedPreferences("security", Context.MODE_PRIVATE);
		Editor editor = preferences.edit();
		editor.putString("AES_KEY", decryptedKeyString);
		editor.putLong("AES_KEY_DATE", new Date().getTime());
		editor.commit();
    }
    
    /**
     * Gets AES key and encrypts it by RSA algorithm
     * @return rsa encrypted AES key
     */
    private String getEncryptedKey(){
    	// read the key
    	String key = serviceServerSecurity.getAESKey();
    	
    	// encrypt the key via RSA algorithm
    	return serviceServerSecurity.encryptStringRSA(key);
    }
    
    private void showDatainGui(){
    	Set<String> keyset =  data.keySet();
    	String showmessage = "\n";
    	for(String s : keyset){
    		showmessage += "\n" + s + ":\n" + data.get(s);
    	}
    	
    	androidContextService.transferData(showmessage);
    }
}
